home *** CD-ROM | disk | FTP | other *** search
/ PC World Komputer 2010 April / PCWorld0410.iso / pluginy Firefox / 10868 / 10868.xpi / modules / engines.js < prev    next >
Text File  |  2010-02-02  |  24KB  |  753 lines

  1. /* ***** BEGIN LICENSE BLOCK *****
  2.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  3.  *
  4.  * The contents of this file are subject to the Mozilla Public License Version
  5.  * 1.1 (the "License"); you may not use this file except in compliance with
  6.  * the License. You may obtain a copy of the License at
  7.  * http://www.mozilla.org/MPL/
  8.  *
  9.  * Software distributed under the License is distributed on an "AS IS" basis,
  10.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  11.  * for the specific language governing rights and limitations under the
  12.  * License.
  13.  *
  14.  * The Original Code is Bookmarks Sync.
  15.  *
  16.  * The Initial Developer of the Original Code is Mozilla.
  17.  * Portions created by the Initial Developer are Copyright (C) 2007
  18.  * the Initial Developer. All Rights Reserved.
  19.  *
  20.  * Contributor(s):
  21.  *  Dan Mills <thunder@mozilla.com>
  22.  *  Myk Melez <myk@mozilla.org>
  23.  *
  24.  * Alternatively, the contents of this file may be used under the terms of
  25.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  26.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  27.  * in which case the provisions of the GPL or the LGPL are applicable instead
  28.  * of those above. If you wish to allow use of your version of this file only
  29.  * under the terms of either the GPL or the LGPL, and not to allow others to
  30.  * use your version of this file under the terms of the MPL, indicate your
  31.  * decision by deleting the provisions above and replace them with the notice
  32.  * and other provisions required by the GPL or the LGPL. If you do not delete
  33.  * the provisions above, a recipient may use your version of this file under
  34.  * the terms of any one of the MPL, the GPL or the LGPL.
  35.  *
  36.  * ***** END LICENSE BLOCK ***** */
  37.  
  38. const EXPORTED_SYMBOLS = ['Engines', 'Engine', 'SyncEngine'];
  39.  
  40. const Cc = Components.classes;
  41. const Ci = Components.interfaces;
  42. const Cr = Components.results;
  43. const Cu = Components.utils;
  44.  
  45. Cu.import("resource://weave/ext/Observers.js");
  46. Cu.import("resource://weave/ext/Sync.js");
  47. Cu.import("resource://weave/log4moz.js");
  48. Cu.import("resource://weave/constants.js");
  49. Cu.import("resource://weave/util.js");
  50. Cu.import("resource://weave/resource.js");
  51. Cu.import("resource://weave/identity.js");
  52. Cu.import("resource://weave/stores.js");
  53. Cu.import("resource://weave/trackers.js");
  54.  
  55. Cu.import("resource://weave/base_records/wbo.js");
  56. Cu.import("resource://weave/base_records/keys.js");
  57. Cu.import("resource://weave/base_records/crypto.js");
  58. Cu.import("resource://weave/base_records/collection.js");
  59.  
  60. // Singleton service, holds registered engines
  61.  
  62. Utils.lazy(this, 'Engines', EngineManagerSvc);
  63.  
  64. function EngineManagerSvc() {
  65.   this._engines = {};
  66.   this._log = Log4Moz.repository.getLogger("Service.Engines");
  67.   this._log.level = Log4Moz.Level[Svc.Prefs.get(
  68.     "log.logger.service.engines", "Debug")];
  69. }
  70. EngineManagerSvc.prototype = {
  71.   get: function EngMgr_get(name) {
  72.     // Return an array of engines if we have an array of names
  73.     if (Utils.isArray(name)) {
  74.       let engines = [];
  75.       name.forEach(function(name) {
  76.         let engine = this.get(name);
  77.         if (engine)
  78.           engines.push(engine);
  79.       }, this);
  80.       return engines;
  81.     }
  82.  
  83.     let engine = this._engines[name];
  84.     if (!engine)
  85.       this._log.debug("Could not get engine: " + name);
  86.     return engine;
  87.   },
  88.   getAll: function EngMgr_getAll() {
  89.     return [engine for ([name, engine] in Iterator(Engines._engines))];
  90.   },
  91.   getEnabled: function EngMgr_getEnabled() {
  92.     return this.getAll().filter(function(engine) engine.enabled);
  93.   },
  94.  
  95.   /**
  96.    * Register an Engine to the service. Alternatively, give an array of engine
  97.    * objects to register.
  98.    *
  99.    * @param engineObject
  100.    *        Engine object used to get an instance of the engine
  101.    * @return The engine object if anything failed
  102.    */
  103.   register: function EngMgr_register(engineObject) {
  104.     if (Utils.isArray(engineObject))
  105.       return engineObject.map(this.register, this);
  106.  
  107.     try {
  108.       let name = engineObject.prototype.name;
  109.       if (name in this._engines)
  110.         this._log.error("Engine '" + name + "' is already registered!");
  111.       else
  112.         this._engines[name] = new engineObject();
  113.     }
  114.     catch(ex) {
  115.       let mesg = ex.message ? ex.message : ex;
  116.       let name = engineObject || "";
  117.       name = name.prototype || "";
  118.       name = name.name || "";
  119.  
  120.       let out = "Could not initialize engine '" + name + "': " + mesg;
  121.       dump(out);
  122.       this._log.error(out);
  123.  
  124.       return engineObject;
  125.     }
  126.   },
  127.   unregister: function EngMgr_unregister(val) {
  128.     let name = val;
  129.     if (val instanceof Engine)
  130.       name = val.name;
  131.     delete this._engines[name];
  132.   }
  133. };
  134.  
  135. function Engine() { this._init(); }
  136. Engine.prototype = {
  137.   name: "engine",
  138.   _displayName: "Boring Engine",
  139.   description: "An engine example - it doesn't actually sync anything",
  140.   logName: "Engine",
  141.  
  142.   // _storeObj, and _trackerObj should to be overridden in subclasses
  143.  
  144.   _storeObj: Store,
  145.   _trackerObj: Tracker,
  146.  
  147.   get prefName() this.name,
  148.   get enabled() Svc.Prefs.get("engine." + this.prefName, null),
  149.   set enabled(val) Svc.Prefs.set("engine." + this.prefName, !!val),
  150.  
  151.   get score() this._tracker.score,
  152.  
  153.   get _store() {
  154.     if (!this.__store)
  155.       this.__store = new this._storeObj();
  156.     return this.__store;
  157.   },
  158.  
  159.   get _tracker() {
  160.     if (!this.__tracker)
  161.       this.__tracker = new this._trackerObj();
  162.     return this.__tracker;
  163.   },
  164.  
  165.   get displayName() {
  166.     try {
  167.       return Str.engines.get(this.name);
  168.     } catch (e) {}
  169.  
  170.     return this._displayName;
  171.   },
  172.  
  173.   _init: function Engine__init() {
  174.     this._notify = Utils.notify("weave:engine:");
  175.     this._log = Log4Moz.repository.getLogger("Engine." + this.logName);
  176.     let level = Svc.Prefs.get("log.logger.engine." + this.name, "Debug");
  177.     this._log.level = Log4Moz.Level[level];
  178.  
  179.     this._tracker; // initialize tracker to load previously changed IDs
  180.     this._log.debug("Engine initialized");
  181.   },
  182.  
  183.   sync: function Engine_sync() {
  184.     if (!this._sync)
  185.       throw "engine does not implement _sync method";
  186.  
  187.     let times = {};
  188.     let wrapped = {};
  189.     // Find functions in any point of the prototype chain
  190.     for (let _name in this) {
  191.       let name = _name;
  192.  
  193.       // Ignore certain constructors/functions
  194.       if (name.search(/^_(.+Obj|notify)$/) == 0)
  195.         continue;
  196.  
  197.       // Only track functions but skip the constructors
  198.       if (typeof this[name] == "function") {
  199.         times[name] = [];
  200.         wrapped[name] = this[name];
  201.  
  202.         // Wrap the original function with a start/stop timer
  203.         this[name] = function() {
  204.           let start = Date.now();
  205.           try {
  206.             return wrapped[name].apply(this, arguments);
  207.           }
  208.           finally {
  209.             times[name].push(Date.now() - start);
  210.           }
  211.         };
  212.       }
  213.     }
  214.  
  215.     try {
  216.       this._notify("sync", this.name, this._sync)();
  217.     }
  218.     finally {
  219.       // Restore original unwrapped functionality
  220.       for (let [name, func] in Iterator(wrapped))
  221.         this[name] = func;
  222.  
  223.       let stats = {};
  224.       for (let [name, time] in Iterator(times)) {
  225.         // Figure out stats on the times unless there's nothing
  226.         let num = time.length;
  227.         if (num == 0)
  228.           continue;
  229.  
  230.         // Track the min/max/sum of the values
  231.         let stat = {
  232.           num: num,
  233.           sum: 0
  234.         };
  235.         time.forEach(function(val) {
  236.           if (stat.min == null || val < stat.min)
  237.             stat.min = val;
  238.           if (stat.max == null || val > stat.max)
  239.             stat.max = val;
  240.           stat.sum += val;
  241.         });
  242.  
  243.         stat.avg = Number((stat.sum / num).toFixed(2));
  244.         stats[name] = stat;
  245.       }
  246.  
  247.       stats.toString = function() {
  248.         let sums = [];
  249.         for (let [name, stat] in Iterator(this))
  250.           if (stat.sum != null)
  251.             sums.push(name.replace(/^_/, "") + " " + stat.sum);
  252.  
  253.         // Order certain functions first before any other random ones
  254.         let nameOrder = ["sync", "processIncoming", "uploadOutgoing",
  255.           "syncStartup", "syncFinish"];
  256.         let getPos = function(str) {
  257.           let pos = nameOrder.indexOf(str.split(" ")[0]);
  258.           return pos != -1 ? pos : Infinity;
  259.         };
  260.         let order = function(a, b) getPos(a) > getPos(b);
  261.  
  262.         return "Total (ms): " + sums.sort(order).join(", ");
  263.       };
  264.  
  265.       this._log.debug(stats);
  266.     }
  267.   },
  268.  
  269.   wipeServer: function Engine_wipeServer() {
  270.     if (!this._wipeServer)
  271.       throw "engine does not implement _wipeServer method";
  272.     this._notify("wipe-server", this.name, this._wipeServer)();
  273.   },
  274.  
  275.   /**
  276.    * Get rid of any local meta-data
  277.    */
  278.   resetClient: function Engine_resetClient() {
  279.     if (!this._resetClient)
  280.       throw "engine does not implement _resetClient method";
  281.  
  282.     this._notify("reset-client", this.name, this._resetClient)();
  283.   },
  284.  
  285.   _wipeClient: function Engine__wipeClient() {
  286.     this.resetClient();
  287.     this._log.debug("Deleting all local data");
  288.     this._store.wipe();
  289.   },
  290.  
  291.   wipeClient: function Engine_wipeClient() {
  292.     this._notify("wipe-client", this.name, this._wipeClient)();
  293.   }
  294. };
  295.  
  296. function SyncEngine() { this._init(); }
  297. SyncEngine.prototype = {
  298.   __proto__: Engine.prototype,
  299.  
  300.   _recordObj: CryptoWrapper,
  301.  
  302.   _init: function _init() {
  303.     Engine.prototype._init.call(this);
  304.     this.loadToFetch();
  305.   },
  306.  
  307.   get storageURL() Svc.Prefs.get("clusterURL") + Svc.Prefs.get("storageAPI") +
  308.     "/" + ID.get("WeaveID").username + "/storage/",
  309.  
  310.   get engineURL() this.storageURL + this.name,
  311.  
  312.   get cryptoMetaURL() this.storageURL + "crypto/" + this.name,
  313.  
  314.   get lastSync() {
  315.     return parseFloat(Svc.Prefs.get(this.name + ".lastSync", "0"));
  316.   },
  317.   set lastSync(value) {
  318.     // Reset the pref in-case it's a number instead of a string
  319.     Svc.Prefs.reset(this.name + ".lastSync");
  320.     // Store the value as a string to keep floating point precision
  321.     Svc.Prefs.set(this.name + ".lastSync", value.toString());
  322.   },
  323.   resetLastSync: function SyncEngine_resetLastSync() {
  324.     this._log.debug("Resetting " + this.name + " last sync time");
  325.     Svc.Prefs.reset(this.name + ".lastSync");
  326.     Svc.Prefs.set(this.name + ".lastSync", "0");
  327.   },
  328.  
  329.   get toFetch() this._toFetch,
  330.   set toFetch(val) {
  331.     this._toFetch = val;
  332.     Utils.jsonSave("toFetch/" + this.name, this, val);
  333.   },
  334.  
  335.   loadToFetch: function loadToFetch() {
  336.     // Initialize to empty if there's no file
  337.     this._toFetch = [];
  338.     Utils.jsonLoad("toFetch/" + this.name, this, Utils.bind2(this, function(o)
  339.       this._toFetch = o));
  340.   },
  341.  
  342.   // Create a new record by querying the store, and add the engine metadata
  343.   _createRecord: function SyncEngine__createRecord(id) {
  344.     return this._store.createRecord(id, this.cryptoMetaURL);
  345.   },
  346.  
  347.   // Any setup that needs to happen at the beginning of each sync.
  348.   // Makes sure crypto records and keys are all set-up
  349.   _syncStartup: function SyncEngine__syncStartup() {
  350.     this._log.trace("Ensuring server crypto records are there");
  351.  
  352.     // Try getting/unwrapping the crypto record
  353.     let meta = CryptoMetas.get(this.cryptoMetaURL);
  354.     if (meta) {
  355.       try {
  356.         let pubkey = PubKeys.getDefaultKey();
  357.         let privkey = PrivKeys.get(pubkey.privateKeyUri);
  358.         meta.getKey(privkey, ID.get("WeaveCryptoID"));
  359.       }
  360.       catch(ex) {
  361.         // Remove traces of this bad cryptometa
  362.         this._log.debug("Purging bad data after failed unwrap crypto: " + ex);
  363.         CryptoMetas.del(this.cryptoMetaURL);
  364.         meta = null;
  365.  
  366.         // Remove any potentially tained data
  367.         new Resource(this.engineURL).delete();
  368.       }
  369.     }
  370.  
  371.     // Generate a new crypto record
  372.     if (!meta) {
  373.       let symkey = Svc.Crypto.generateRandomKey();
  374.       let pubkey = PubKeys.getDefaultKey();
  375.       meta = new CryptoMeta(this.cryptoMetaURL);
  376.       meta.generateIV();
  377.       meta.addUnwrappedKey(pubkey, symkey);
  378.       let res = new Resource(meta.uri);
  379.       let resp = res.put(meta);
  380.       if (!resp.success) {
  381.         this._log.debug("Metarecord upload fail:" + resp);
  382.         resp.failureCode = ENGINE_METARECORD_UPLOAD_FAIL;
  383.         throw resp;
  384.       }
  385.  
  386.       // Cache the cryto meta that we just put on the server
  387.       CryptoMetas.set(meta.uri, meta);
  388.     }
  389.  
  390.     // first sync special case: upload all items
  391.     // NOTE: we use a backdoor (of sorts) to the tracker so it
  392.     // won't save to disk this list over and over
  393.     if (!this.lastSync) {
  394.       this._log.debug("First sync, uploading all items");
  395.       this._tracker.clearChangedIDs();
  396.       [i for (i in this._store.getAllIDs())]
  397.         .forEach(function(id) this._tracker.changedIDs[id] = true, this);
  398.     }
  399.  
  400.     let outnum = [i for (i in this._tracker.changedIDs)].length;
  401.     this._log.info(outnum + " outgoing items pre-reconciliation");
  402.  
  403.     // Keep track of what to delete at the end of sync
  404.     this._delete = {};
  405.   },
  406.  
  407.   // Generate outgoing records
  408.   _processIncoming: function SyncEngine__processIncoming() {
  409.     this._log.trace("Downloading & applying server changes");
  410.  
  411.     // Figure out how many total items to fetch this sync; do less on mobile
  412.     let fetchNum = 1500;
  413.     if (Svc.Prefs.get("client.type") == "mobile")
  414.       fetchNum = 50;
  415.  
  416.     // enable cache, and keep only the first few items.  Otherwise (when
  417.     // we have more outgoing items than can fit in the cache), we will
  418.     // keep rotating items in and out, perpetually getting cache misses
  419.     this._store.cache.enabled = true;
  420.     this._store.cache.fifo = false; // filo
  421.     this._store.cache.clear();
  422.  
  423.     let newitems = new Collection(this.engineURL, this._recordObj);
  424.     newitems.newer = this.lastSync;
  425.     newitems.full = true;
  426.     newitems.sort = "index";
  427.     newitems.limit = fetchNum;
  428.  
  429.     let count = {applied: 0, reconciled: 0};
  430.     let handled = [];
  431.     newitems.recordHandler = Utils.bind2(this, function(item) {
  432.       // Grab a later last modified if possible
  433.       if (this.lastModified == null || item.modified > this.lastModified)
  434.         this.lastModified = item.modified;
  435.  
  436.       // Remember which records were processed
  437.       handled.push(item.id);
  438.  
  439.       try {
  440.         item.decrypt(ID.get("WeaveCryptoID"));
  441.         if (this._reconcile(item)) {
  442.           count.applied++;
  443.           this._tracker.ignoreAll = true;
  444.           this._store.applyIncoming(item);
  445.         } else {
  446.           count.reconciled++;
  447.           this._log.trace("Skipping reconciled incoming item " + item.id);
  448.         }
  449.       }
  450.       catch(ex) {
  451.         this._log.warn("Error processing record: " + Utils.exceptionStr(ex));
  452.       }
  453.       this._tracker.ignoreAll = false;
  454.       Sync.sleep(0);
  455.     });
  456.  
  457.     // Only bother getting data from the server if there's new things
  458.     if (this.lastModified == null || this.lastModified > this.lastSync) {
  459.       let resp = newitems.get();
  460.       if (!resp.success) {
  461.         resp.failureCode = ENGINE_DOWNLOAD_FAIL;
  462.         throw resp;
  463.       }
  464.  
  465.       // Subtract out the number of items we just got
  466.       fetchNum -= handled.length;
  467.     }
  468.  
  469.     // Check if we got the maximum that we requested; get the rest if so
  470.     if (handled.length == newitems.limit) {
  471.       let guidColl = new Collection(this.engineURL);
  472.       guidColl.newer = this.lastSync;
  473.       guidColl.sort = "index";
  474.  
  475.       let guids = guidColl.get();
  476.       if (!guids.success)
  477.         throw guids;
  478.  
  479.       // Figure out which guids weren't just fetched then remove any guids that
  480.       // were already waiting and prepend the new ones
  481.       let extra = Utils.arraySub(guids.obj, handled);
  482.       if (extra.length > 0)
  483.         this.toFetch = extra.concat(Utils.arraySub(this.toFetch, extra));
  484.     }
  485.  
  486.     // Process any backlog of GUIDs if we haven't fetched too many this sync
  487.     while (this.toFetch.length > 0 && fetchNum > 0) {
  488.       // Reuse the original query, but get rid of the restricting params
  489.       newitems.limit = 0;
  490.       newitems.newer = 0;
  491.  
  492.       // Get the first bunch of records and save the rest for later
  493.       let minFetch = Math.min(150, this.toFetch.length, fetchNum);
  494.       newitems.ids = this.toFetch.slice(0, minFetch);
  495.       this.toFetch = this.toFetch.slice(minFetch);
  496.       fetchNum -= minFetch;
  497.  
  498.       // Reuse the existing record handler set earlier
  499.       let resp = newitems.get();
  500.       if (!resp.success) {
  501.         resp.failureCode = ENGINE_DOWNLOAD_FAIL;
  502.         throw resp;
  503.       }
  504.     }
  505.  
  506.     if (this.lastSync < this.lastModified)
  507.       this.lastSync = this.lastModified;
  508.  
  509.     this._log.info(["Records:", count.applied, "applied,", count.reconciled,
  510.       "reconciled,", this.toFetch.length, "left to fetch"].join(" "));
  511.  
  512.     // try to free some memory
  513.     this._store.cache.clear();
  514.   },
  515.  
  516.   /**
  517.    * Find a GUID of an item that is a duplicate of the incoming item but happens
  518.    * to have a different GUID
  519.    *
  520.    * @return GUID of the similar item; falsy otherwise
  521.    */
  522.   _findDupe: function _findDupe(item) {
  523.     // By default, assume there's no dupe items for the engine
  524.   },
  525.  
  526.   _isEqual: function SyncEngine__isEqual(item) {
  527.     let local = this._createRecord(item.id);
  528.     if (this._log.level <= Log4Moz.Level.Trace)
  529.       this._log.trace("Local record: " + local);
  530.     if (item.parentid == local.parentid &&
  531.         item.deleted == local.deleted &&
  532.         Utils.deepEquals(item.cleartext, local.cleartext)) {
  533.       this._log.trace("Local record is the same");
  534.       return true;
  535.     } else {
  536.       this._log.trace("Local record is different");
  537.       return false;
  538.     }
  539.   },
  540.  
  541.   _deleteId: function _deleteId(id) {
  542.     this._tracker.removeChangedID(id);
  543.  
  544.     // Remember this id to delete at the end of sync
  545.     if (this._delete.ids == null)
  546.       this._delete.ids = [id];
  547.     else
  548.       this._delete.ids.push(id);
  549.   },
  550.  
  551.   _handleDupe: function _handleDupe(item, dupeId) {
  552.     // The local dupe is the lower id, so pretend the incoming is for it
  553.     if (dupeId < item.id) {
  554.       this._deleteId(item.id);
  555.       item.id = dupeId;
  556.       this._tracker.changedIDs[dupeId] = true;
  557.     }
  558.     // The incoming item has the lower id, so change the dupe to it
  559.     else {
  560.       this._store.changeItemID(dupeId, item.id);
  561.       this._deleteId(dupeId);
  562.     }
  563.  
  564.     this._store.cache.clear(); // because parentid refs will be wrong
  565.   },
  566.  
  567.   // Reconciliation has three steps:
  568.   // 1) Check for the same item (same ID) on both the incoming and outgoing
  569.   //    queues.  This means the same item was modified on this profile and
  570.   //    another at the same time.  In this case, this client wins (which really
  571.   //    means, the last profile you sync wins).
  572.   // 2) Check if the incoming item's ID exists locally.  In that case it's an
  573.   //    update and we should not try a similarity check (step 3)
  574.   // 3) Check if any incoming & outgoing items are actually the same, even
  575.   //    though they have different IDs.  This happens when the same item is
  576.   //    added on two different machines at the same time.  It's also the common
  577.   //    case when syncing for the first time two machines that already have the
  578.   //    same bookmarks.  In this case we change the IDs to match.
  579.   _reconcile: function SyncEngine__reconcile(item) {
  580.     if (this._log.level <= Log4Moz.Level.Trace)
  581.       this._log.trace("Incoming: " + item);
  582.  
  583.     // Step 1: Check for conflicts
  584.     //         If same as local record, do not upload
  585.     this._log.trace("Reconcile step 1");
  586.     if (item.id in this._tracker.changedIDs) {
  587.       if (this._isEqual(item))
  588.         this._tracker.removeChangedID(item.id);
  589.       return false;
  590.     }
  591.  
  592.     // Step 2: Check for updates
  593.     //         If different from local record, apply server update
  594.     this._log.trace("Reconcile step 2");
  595.     if (this._store.itemExists(item.id))
  596.       return !this._isEqual(item);
  597.  
  598.     // If the incoming item has been deleted, skip step 3
  599.     this._log.trace("Reconcile step 2.5");
  600.     if (item.deleted)
  601.       return true;
  602.  
  603.     // Step 3: Check for similar items
  604.     this._log.trace("Reconcile step 3");
  605.     let dupeId = this._findDupe(item);
  606.     if (dupeId)
  607.       this._handleDupe(item, dupeId);
  608.  
  609.     // Apply the incoming item (now that the dupe is the right id)
  610.     return true;
  611.   },
  612.  
  613.   // Upload outgoing records
  614.   _uploadOutgoing: function SyncEngine__uploadOutgoing() {
  615.     let outnum = [i for (i in this._tracker.changedIDs)].length;
  616.     if (outnum) {
  617.       this._log.trace("Preparing " + outnum + " outgoing records");
  618.  
  619.       // collection we'll upload
  620.       let up = new Collection(this.engineURL);
  621.       let count = 0;
  622.  
  623.       // Upload what we've got so far in the collection
  624.       let doUpload = Utils.bind2(this, function(desc) {
  625.         this._log.info("Uploading " + desc + " of " + outnum + " records");
  626.         let resp = up.post();
  627.         if (!resp.success) {
  628.           this._log.debug("Uploading records failed: " + resp);
  629.           resp.failureCode = ENGINE_UPLOAD_FAIL;
  630.           throw resp;
  631.         }
  632.  
  633.         // Record the modified time of the upload
  634.         let modified = resp.headers["X-Weave-Timestamp"];
  635.         if (modified > this.lastSync)
  636.           this.lastSync = modified;
  637.  
  638.         up.clearRecords();
  639.       });
  640.  
  641.       // don't cache the outgoing items, we won't need them later
  642.       this._store.cache.enabled = false;
  643.  
  644.       for (let id in this._tracker.changedIDs) {
  645.         try {
  646.           let out = this._createRecord(id);
  647.           if (this._log.level <= Log4Moz.Level.Trace)
  648.             this._log.trace("Outgoing: " + out);
  649.  
  650.           out.encrypt(ID.get("WeaveCryptoID"));
  651.           up.pushData(out);
  652.         }
  653.         catch(ex) {
  654.           this._log.warn("Error creating record: " + Utils.exceptionStr(ex));
  655.         }
  656.  
  657.         // Partial upload
  658.         if ((++count % MAX_UPLOAD_RECORDS) == 0)
  659.           doUpload((count - MAX_UPLOAD_RECORDS) + " - " + count + " out");
  660.  
  661.         Sync.sleep(0);
  662.       }
  663.  
  664.       // Final upload
  665.       if (count % MAX_UPLOAD_RECORDS > 0)
  666.         doUpload(count >= MAX_UPLOAD_RECORDS ? "last batch" : "all");
  667.  
  668.       this._store.cache.enabled = true;
  669.     }
  670.     this._tracker.clearChangedIDs();
  671.   },
  672.  
  673.   // Any cleanup necessary.
  674.   // Save the current snapshot so as to calculate changes at next sync
  675.   _syncFinish: function SyncEngine__syncFinish() {
  676.     this._log.trace("Finishing up sync");
  677.     this._tracker.resetScore();
  678.  
  679.     let doDelete = Utils.bind2(this, function(key, val) {
  680.       let coll = new Collection(this.engineURL, this._recordObj);
  681.       coll[key] = val;
  682.       coll.delete();
  683.     });
  684.  
  685.     for (let [key, val] in Iterator(this._delete)) {
  686.       // Remove the key for future uses
  687.       delete this._delete[key];
  688.  
  689.       // Send a simple delete for the property
  690.       if (key != "ids" || val.length <= 100)
  691.         doDelete(key, val);
  692.       else {
  693.         // For many ids, split into chunks of at most 100
  694.         while (val.length > 0) {
  695.           doDelete(key, val.slice(0, 100));
  696.           val = val.slice(100);
  697.         }
  698.       }
  699.     }
  700.   },
  701.  
  702.   _sync: function SyncEngine__sync() {
  703.     try {
  704.       this._syncStartup();
  705.       Observers.notify("weave:engine:sync:status", "process-incoming");
  706.       this._processIncoming();
  707.       Observers.notify("weave:engine:sync:status", "upload-outgoing");
  708.       this._uploadOutgoing();
  709.       this._syncFinish();
  710.     }
  711.     catch (e) {
  712.       this._log.warn("Sync failed");
  713.       throw e;
  714.     }
  715.   },
  716.  
  717.   _wipeServer: function SyncEngine__wipeServer() {
  718.     new Resource(this.engineURL).delete();
  719.     new Resource(this.cryptoMetaURL).delete();
  720.   },
  721.  
  722.   _testDecrypt: function _testDecrypt() {
  723.     // Report failure even if there's nothing to decrypt
  724.     let canDecrypt = false;
  725.  
  726.     // Fetch the most recently uploaded record and try to decrypt it
  727.     let test = new Collection(this.engineURL, this._recordObj);
  728.     test.limit = 1;
  729.     test.sort = "newest";
  730.     test.full = true;
  731.     test.recordHandler = function(record) {
  732.       record.decrypt(ID.get("WeaveCryptoID"));
  733.       canDecrypt = true;
  734.     };
  735.  
  736.     // Any failure fetching/decrypting will just result in false
  737.     try {
  738.       this._log.trace("Trying to decrypt a record from the server..");
  739.       test.get();
  740.     }
  741.     catch(ex) {
  742.       this._log.debug("Failed test decrypt: " + Utils.exceptionStr(ex));
  743.     }
  744.  
  745.     return canDecrypt;
  746.   },
  747.  
  748.   _resetClient: function SyncEngine__resetClient() {
  749.     this.resetLastSync();
  750.     this.toFetch = [];
  751.   }
  752. };
  753.